﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using MicroDal.Context;
using MicroDal.Providers;
using System.Linq.Expressions;

namespace MicroDal
{
    /// <summary>
    /// This is the main (and only) object in MicroDal - it holds a data-session
    /// </summary>
    public sealed class DataSession : IDisposable
    {
        private IDbResource _resource;
        internal static MapCache _mapCache = new MapCache();
        private SqlParser query;


        /// <summary>
        /// Constructor used for creating a session.
        /// </summary>
        /// <param name="connectionName">Name from configuration section</param>
        public DataSession(string connectionName)
        {
            if (string.IsNullOrEmpty(connectionName))
                throw new ArgumentException("ConnectionName can not be null or empty.", "connectionName");

            this._resource = DbResourceFactory.Create(connectionName);
            Tracer.WriteTrace("DataSession constructed with resource: {0}", this._resource.GetType().Name);
            query = new SqlParser(_resource);

            this.DbResourceCall += (s, ea) =>
            {
                Tracer.WriteMessage(ea.SqlQuery);
                Tracer.WriteParametersIfVerbose(ea.Parameters);
            };
            _disposed = false;
        }

        public DataSession(string connectionstring, string providerName)
        {
            this._resource = DbResourceFactory.Create(connectionstring, providerName);

            Tracer.WriteTrace("DataSession constructed with resource: {0}", this._resource.GetType().Name);
            query = new SqlParser(_resource);

            this.DbResourceCall += (s, ea) =>
            {
                Tracer.WriteMessage(ea.SqlQuery);
                Tracer.WriteParametersIfVerbose(ea.Parameters);
            };
            _disposed = false;
        }

        public DataSession(IDbResource resource)
        {
            this._resource = resource;
            Tracer.WriteTrace("DataSession constructed with resource: {0}", this._resource.GetType().Name);
            query = new SqlParser(_resource);

            this.DbResourceCall += (s, ea) =>
            {
                Tracer.WriteMessage(ea.SqlQuery);
                Tracer.WriteParametersIfVerbose(ea.Parameters);
            };
            _disposed = false;
        }

        /// <summary>
        /// This event fires, just before the call to the underlying IDbResource
        /// </summary>
        public event EventHandler<DbResourceCallEventArgs> DbResourceCall;

        private void OnDbResourceCall(string query, object[] parameters)
        {
            OnDbResourceCall(new DbResourceCallEventArgs(query, parameters));
        }
        private void OnDbResourceCall(DbResourceCallEventArgs ea)
        {
            if (DbResourceCall != null)
                DbResourceCall(this, ea);
        }

        /// <summary>
        /// This event fires when a datavalue is returned from the underlying IDbResource
        /// The event will fire multiple times for Fetch, and only once fore FetchOne, FetchScalar and Execute
        /// </summary>
        public event EventHandler<DataReturnedEventArgs> DataReturned;
        private void OnDataReturned(DataReturnedEventArgs ea)
        {
            if (DataReturned != null)
                DataReturned(this, ea);
        }
        private void OnDataReturned(object data)
        {
            if (DataReturned != null)
                DataReturned(this, new DataReturnedEventArgs { Data = data });
        }

        #region FetchOne<T>
        /// <summary>
        /// Returns a dynamic object, that have properties similar to the columns returned in data.
        /// </summary>
        /// <param name="sql">Valid SQL</param>
        /// <param name="values">Param-array of parameters - should matc in length the count of @xx's in the SQL string</param>
        /// <returns>Initialized dynamic object with values from DB</returns>
        public dynamic FetchOne(string sql, params object[] values)
        {
            return FetchOne<dynamic>(sql, DynamicMapper.Map, values);
        }

        /// <summary>
        /// Returns one object from the provided query and the optional parameters
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sql">Valid SQL</param>
        /// <param name="values">Param-array of parameters - should matc in length the count of @xx's in the SQL string</param>
        /// <returns>Initialized object of type T with values from DB</returns>
        public T FetchOne<T>(string sql, params object[] values) where T : new()
        {
            return FetchOne<T>(sql, _mapCache.Get<T>(), values);
        }

        /// <summary>
        /// Returns one object from the provided query and the optional parameters
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sql">Valid SQL</param>
        /// <param name="map">A function taking a DbDataReader and returning a initialized object of type T</param>
        /// <param name="values">Param-array of parameters - should matc in length the count of @xx's in the SQL string</param>
        /// <returns>Initialized object of type T with values from DB</returns>
        public T FetchOne<T>(string sql, Func<DbDataReader, T> map, params object[] values)
        {
            if (_disposed)
                throw new ObjectDisposedException("DataSession was disposed.");

            if (string.IsNullOrEmpty(sql))
                throw new ArgumentException("SQL query should not be null or empty", "sql");
            if (map == null)
                throw new ArgumentNullException("map");

            query.Parse(sql);

            var parameters = query.GetParameterEnumerator(values);

            OnDbResourceCall(query.Query, values);
            T result = _resource.FetchOne<T>(query.Query, query.CommandType, parameters, map);
            OnDataReturned(new DataReturnedEventArgs { Data = result });
            return result;
        }

        #endregion

        #region Fetch<T>
        /// <summary>
        /// Returns a ienumerable collection of dynamic objects that have properties similar to the columns returned in data.
        /// </summary>
        public IEnumerable<dynamic> Fetch(string sql, params object[] values)
        {
            return Fetch<dynamic>(sql, DynamicMapper.Map, values);
        }

        /// <summary>
        /// Returns a IEnumerabel collection of objects from the provided query and the optional parameters
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sql">Valid SQL</param>
        /// <param name="values">Param-array of parameters - should matc in length the count of @xx's in the SQL string</param>
        /// <returns>Enumerable of Initialized object of type T with values from DB</returns>
        public IEnumerable<T> Fetch<T>(string sql, params object[] values) where T : new()
        {
            return Fetch<T>(sql, _mapCache.Get<T>(), values);
        }

        /// <summary>
        /// Returns a enumerabel collection of objects from the provided query and the optional parameters
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sql">Valid SQL</param>
        /// <param name="map">A function taking a DbDataReader and returning a initialized object of type T</param>
        /// <param name="values">Param-array of parameters - should matc in length the count of @xx's in the SQL string</param>
        /// <returns>Initialized object of type T with values from DB</returns>
        public IEnumerable<T> Fetch<T>(string sql, Func<DbDataReader, T> map, params object[] values)
        {
            if (_disposed)
                throw new ObjectDisposedException("DataSession was disposed.");
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentException("SQL query should not be null or empty", "sql");
            if (map == null)
                throw new ArgumentNullException("map");

            query.Parse(sql);
            var parameters = query.GetParameterEnumerator(values);

            OnDbResourceCall(sql, values);
            return FetchImpl<T>(query.Query, query.CommandType, parameters, map);
        }

        private IEnumerable<T> FetchImpl<T>(string sql, CommandType commandType, IEnumerable<DbParameter> parameters, Func<DbDataReader, T> map)
        {
            foreach (var item in _resource.Fetch<T>(sql, commandType, parameters, map))
            {
                yield return item;
                OnDataReturned(item);
            }
        }
        #endregion

        #region Execute
        /// <summary>
        /// Used to execute a 'non-query' against the DB.
        /// </summary>
        /// <param name="sql">Valid SQL</param>
        /// <param name="values">Param-array of parameters - should matc in length the count of @xx's in the SQL string</param>
        /// <returns>Number of rows affected in the DB</returns>
        public int Execute(string sql, params object[] values)
        {
            if (_disposed)
                throw new ObjectDisposedException("DataSession was disposed.");
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentException("SQL query should not be null or empty", "sql");

            query.Parse(sql);
            var parameters = query.GetParameterEnumerator(values);

            OnDbResourceCall(sql, values);
            int result = _resource.Execute(query.Query, query.CommandType, parameters);
            OnDataReturned(new DataReturnedEventArgs { Data = result });
            return result;
        }
        #endregion

        #region FetchScalar
        /// <summary>
        /// Executes the SQL query, and returns a scalar object of type T as a result
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sql">Valid SQL</param>
        /// <param name="values">Param-array of parameters - should matc in length the count of @xx's in the SQL string</param>
        /// <returns></returns>
        public T FetchScalar<T>(string sql, params object[] values)
        {
            if (_disposed)
                throw new ObjectDisposedException("DataSession was disposed.");
            if (string.IsNullOrEmpty(sql))
                throw new ArgumentException("SQL query should not be null or empty", "sql");

            query.Parse(sql);
            var parameters = query.GetParameterEnumerator(values);

            OnDbResourceCall(sql, values);
            var scalarval = _resource.ExecuteScalar(query.Query, query.CommandType, parameters);

            T result = (typeof(T).IsValueType ? (T)Activator.CreateInstance(typeof(T)) : default(T));

            if (scalarval != null && scalarval != DBNull.Value)
                result = (T)scalarval;

            OnDataReturned(new DataReturnedEventArgs { Data = result });
            return result;
        }
        #endregion

        /// <summary>
        /// Returns the last inserted id. Remark: Some providers can return id's from other session, or from triggers.
        /// Some providers return id's bigger than Int64 (numeric(38,0))
        /// </summary>
        public T GetLastInsertedId<T>()
        {
            if (_disposed)
                throw new ObjectDisposedException("DataSession was disposed.");
            return _resource.GetLastInsertedId<T>();
        }

        private TransactionalScope _currentTransactionalScope;
        public TransactionalScope GetTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
        {
            if (_disposed)
                throw new ObjectDisposedException("DataSession was disposed.");
            if (this.HasOpenTransaction)
                throw new InvalidOperationException("Nested transactions not supported.");

            return _currentTransactionalScope = new TransactionalScope(this, isolationLevel);
        }

        public bool HasOpenTransaction
        {
            get
            {
                return _currentTransactionalScope != null && _currentTransactionalScope.IsOpen;
            }
        }

        public void SetMapper<T>(Expression<Func<DbDataReader, T>> mapper)
        {
            _mapCache.Bind(typeof(T).GetHashCode(), mapper);
        }

        #region IDisposable Members
        /// <summary>
        /// Releases all ressources
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
        }

        public void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    if (_resource != null)
                        _resource.Dispose();
                    Tracer.WriteTrace("DataSession._ressource disposed");
                }
                _resource = null;
                _disposed = true;
            }
        }

        /// <summary>
        /// Check to see if the object is 'disposing'
        /// </summary>
        public bool IsDisposed
        {
            get { return _disposed; }
        }
        private volatile bool _disposed = false;
        #endregion

        /// <summary>
        /// Point of entry - here You get a session from the currently selected context
        /// </summary>
        public static DataSession Current
        {
            get
            {
                // We get the session from the factory unless it's overridden
                return _Current ?? SessionFactory.GetCurrent();
            }
        }
        private static DataSession _Current;

        public static DataSession Get(string connectionname)
        {
            return SessionFactory.Get(connectionname);
        }

        /// <summary>
        /// Gives access to set the singleton instance. This is only intended for testing use
        /// </summary>
        /// <param name="override">Alternative session</param>
        public static void OverrideSessionStorrage(DataSession @override)
        {
            _Current = @override;
        }

        internal IDbResource CurrentResource
        {
            get { return _resource; }
        }
    }
}
